﻿using System;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.Linq.Expressions;
using System.Reflection;
using Cloak.Globalization;

namespace Cloak.Mvvm.Bindings
{
	/// <summary>
	/// A reference to an object's property which may change over time
	/// </summary>
	public sealed class PropertyReference : IPropertyReference
	{
		private readonly INotifyPropertyChanged _source;
		private readonly PropertyInfo _property;

		/// <summary>
		/// Initializes a property reference with the property accessed by the specified getter
		/// </summary>
		/// <param name="getter">The lambda expression which access the referenced property</param>
		public PropertyReference(Expression<Func<object>> getter)
		{
			Contract.Requires(getter != null);

			var memberExpression = GetMemberExpression(getter);

			_source = GetSource(memberExpression);
			_property = GetProperty(_source.GetType(), memberExpression);

			_source.PropertyChanged += OnSourcePropertyChanged;
		}

		#region IPropertyReference
		/// <summary>
		/// Occurs when the value of the <see cref="Value"/> changes
		/// </summary>
		public event EventHandler Changed;

		private void OnChanged(EventArgs e)
		{
			var handler = Changed;

			if(handler != null)
			{
				handler(this, e);
			}
		}

		/// <summary>
		/// Gets or sets the value of the object's property
		/// </summary>
		public object Value
		{
			get { return _property.GetValue(_source, null); }
			set { _property.SetValue(_source, value, null); }
		}

		/// <summary>
		/// Determines whether the value of the specified property is not equal to the value of this property
		/// </summary>
		/// <param name="otherProperty">The property whose value will be compared to the value of this property</param>
		/// <returns>Whether the value of the specified property does not equal the value of this property</returns>
		public bool ValueDiffersFrom(IPropertyReference otherProperty)
		{
			return !ValueEquals(otherProperty.Value);
		}
		#endregion

		#region Getter

		private static MemberExpression GetMemberExpression(Expression<Func<object>> getter)
		{
			var body = getter.Body;

			if(body.NodeType == ExpressionType.Convert && body.Type == typeof(object))
			{
				// Skip past the implicit conversion to the return type of Func<object> to get to the property access
				body = ((UnaryExpression) body).Operand;
			}

			var memberExpression = body as MemberExpression;

			if(memberExpression == null)
			{
				throw new ArgumentException("Getter is not a property acces: {0}".FormatInvariant(getter), "getter");
			}

			return memberExpression;
		}

		private static INotifyPropertyChanged GetSource(MemberExpression memberExpression)
		{
			// This assumes that only leaf node properties will change.
			//
			// Change notification for properties which have bound sub-properties is not supported.

			var getSource = Expression.Lambda<Func<object>>(memberExpression.Expression).Compile();

			var source = getSource();

			if(source == null)
			{
				throw new ArgumentException("Getter cannot return a null source", "memberExpression");
			}

			var notifyingSource = source as INotifyPropertyChanged;

			if(notifyingSource == null)
			{
				throw new ArgumentException(
					"Type {0} does not implement {1}".FormatInvariant(source.GetType(), typeof(INotifyPropertyChanged)),
					"memberExpression");
			}

			return notifyingSource;
		}

		private static PropertyInfo GetProperty(Type sourceType, MemberExpression memberExpression)
		{
			var property = memberExpression.Member as PropertyInfo;

			if(property == null)
			{
				throw new ArgumentException("Getter is not a property access: {0}".FormatInvariant(memberExpression), "memberExpression");
			}

			if(!property.ReflectedType.IsAssignableFrom(sourceType))
			{
				throw new ArgumentException("{0} is not a valid property on source type {1}".FormatInvariant(property.Name, sourceType), "memberExpression");
			}

			if(!HasPublicGetAndSetMethods(property))
			{
				throw new ArgumentException(
					"Property {0} on source type {1} does not have a public getter and setter".FormatInvariant(property.Name, sourceType),
					"memberExpression");
			}

			return property;
		}
		#endregion

		private void OnSourcePropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			if(e.PropertyName == _property.Name)
			{
				OnChanged(EventArgs.Empty);
			}
		}

		private bool ValueEquals(object otherValue)
		{
			return Object.ReferenceEquals(Value, otherValue) || (Value != null && otherValue != null && Value.Equals(otherValue));
		}

		private static bool HasPublicGetAndSetMethods(PropertyInfo property)
		{
			return property.GetGetMethod(false) != null && property.GetSetMethod(false) != null;
		}
	}
}